﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;

using King.Reflection;

namespace King.Extensions {

    [AttributeUsage(AttributeTargets.All, Inherited = true)]
    public class XoTypeAttribute : Attribute {

        private string m_namespace;

        public XoTypeAttribute() {
        }
        public XoTypeAttribute(string xmlNamespace) {
            m_namespace = xmlNamespace;
        }

        public bool NamesStartInLowerCase { get; set; }
        public string Namespace {
            get { return m_namespace; }
        }
    }
    [AttributeUsage(AttributeTargets.All, Inherited = true)]
    public class XoMemberAttribute : Attribute {

        private string m_xpath;

        public XoMemberAttribute() {
        }
        public XoMemberAttribute(string xpath) {
            m_xpath = xpath;
        }

        public string XPath {
            get { return m_xpath; }
        }
        public bool IsAttribute { get; set; }
    }
    [AttributeUsage(AttributeTargets.All, Inherited = true)]
    public class XoXmlAttribute : Attribute {
    }

    public class XoLoader {

        private static Dictionary<Type, XoType> m_typesByType;
        public static XoType GetType(Type type) {

            if (!type.IsDefined<XoTypeAttribute>())
                throw new ArgumentException();

            if (m_typesByType == null)
                m_typesByType = new Dictionary<Type, XoType>();

            var result = m_typesByType.GetValueOrDefault(type);
            if (result == null)
                result = m_typesByType[type] = new XoType(type);

            return result;
        }
        public static XoMember GetMember(PropertyInfo property) {
            var type = GetType(property.DeclaringType);
            if (type == null)
                return null;
            return type.GetMember(property);
        }
    }
    public class XoType {

        private Type m_type;
        private Dictionary<PropertyInfo, XoMember> m_membersByPropertyInfo;
        private XNamespace m_namespace;
        private PropertyInfo m_xmlProperty;
        private bool m_namesStartInLowerCase;

        public XoType(Type type) {
            m_type = type;
            var attribute = type.GetAttribute<XoTypeAttribute>();
            m_namespace = (XNamespace)attribute.Namespace;
            var properties =
                from o in type.Get<PropertyInfo>()
                let getter = o.GetGetMethod(true)
                where getter != null
                where o.IsDefined<XoXmlAttribute>() || getter.IsDefined<XoXmlAttribute>()
                select o;
            m_xmlProperty = properties.Single();
            m_namesStartInLowerCase = attribute.NamesStartInLowerCase;
        }

        public Type Type {
            get { return m_type; }
        }
        public IEnumerable<XoMember> Members() {
            if (m_membersByPropertyInfo == null) {
                var members =
                   (from t in m_type.ToLinkList(o => o.BaseType)
                    from p in t.Get<PropertyInfo>()
                    where p.IsDefined<XoMemberAttribute>()
                    select new XoMember(this, p)).ToArray();
                m_membersByPropertyInfo = members.ToDictionary(o => o.PropertyInfo);
            }
            return m_membersByPropertyInfo.Values;
        }
        public XoMember GetMember(PropertyInfo method) {
            if (m_membersByPropertyInfo == null)
                Members();
            return m_membersByPropertyInfo.GetValueOrDefault(method);
        }
        public XNamespace Namespace {
            get { return m_namespace; }
        }
        public XElement GetXml(object instance) {
            var xml = (XElement)m_xmlProperty.GetValue(instance);
            return xml;
        }
        public bool NamesStartInLowerCase {
            get { return m_namesStartInLowerCase; }
        }
    }
    public class XoMember {

        public static T GetValue<T>(MethodBase method, object instance) {
            var member = XoLoader.GetMember(method.GetProperty());
            if (member == null)
                throw new ArgumentException();
            return member.GetValue<T>(instance);
        }
        public static string GetValue(MethodBase method, object instance) {
            return GetValue<string>(method, instance);
        }

        public static void SetValue<T>(MethodBase method, object instance, T value) {
            var member = XoLoader.GetMember(method.GetProperty());
            if (member == null)
                throw new ArgumentException();
            member.SetValue<T>(instance, value);
        }
        public static void SetValue(MethodBase method, object instance, string value) {
            SetValue<string>(method, instance, value);
        }

        private XoType m_type;
        private PropertyInfo m_propertyInfo;
        private string m_xpath;

        internal XoMember(XoType type, PropertyInfo propertyInfo) {
            m_type = type;
            m_propertyInfo = propertyInfo;
            var attribute = propertyInfo.GetAttribute<XoMemberAttribute>();
            m_xpath = attribute.XPath;
            if (m_xpath == null) {
                if (attribute.IsAttribute)
                    m_xpath = "@";
                else
                    m_xpath = "/";
                var name = propertyInfo.Name;
                if (m_type.NamesStartInLowerCase)
                    name = name.ToLowerFirst();
                m_xpath = m_xpath + name;
            }
        }

        private XObject GetXObject(object instance) {
            var xpath = "xml://xpath/" + m_xpath;
            var xml = m_type.GetXml(instance);
            var node = xml.GetNode(m_type.Namespace, new Uri(xpath));
            return node;
        }

        private string XObjectToString(XObject node) {
            string value;
            if (node is XElement)
                value = (string)(XElement)node;
            else
                value = (string)(XAttribute)node;

            if (value != null)
                value = value.Trim();
            if (string.IsNullOrEmpty(value))
                value = null;

            return value;
        }
        private void StringToXObject(XObject node, string value) {
            if (node == null)
                throw new InvalidOperationException();

            if (node is XAttribute)
                ((XAttribute)node).Value = value;
            else
                ((XElement)node).Value = value;
        }

        private T DeserializeValue<T>(XObject node) {
            var value = XObjectToString(node);

            object result = null;

            var type = typeof(T).MakeNotNullable();
            if (type == typeof(bool)) {
                if (value == null)
                    result = false;
                else
                    result = bool.Parse(value);

            } else if (value != null) {
                if (type == typeof(double)) {
                    result = double.Parse(value);

                } else if (type == typeof(int)) {
                    result = int.Parse(value);

                } else if (type == typeof(Guid)) {
                    result = new Guid(value);

                } else if (type == typeof(string)) {
                    result = value;

                } else
                    throw new NotSupportedException();
            }

            return (T)(object)result;
        }
        private void SerializeValue<T>(object instance, XObject node, T value) {

            string result = null;
            if (typeof(T) == typeof(double))
                result = ((double)(object)value).ToString("#.0");
            else if (typeof(T) == typeof(bool))
                result = value.ToString().ToLower();
            else if (typeof(T) == typeof(string))
                result = value.ToString();
            else
                throw new ArgumentException();

            StringToXObject(node, result);
        }

        public T GetValue<T>(object instance) {
            var node = GetXObject(instance);
            if (node == null)
                node = null;
            return DeserializeValue<T>(node);
        }
        public string GetValue(object instance) {
            return GetValue<string>(instance);
        }

        public void SetValue<T>(object instance, T value) {
            var node = GetXObject(instance);
            if (node == null)
                throw new ArgumentException();

            SerializeValue(instance, node, value);
        }
        public void SetValue(object instance, string value) {
            SetValue<string>(instance, value);
        }

        public XoType Type {
            get { return m_type; }
        }
        public PropertyInfo PropertyInfo {
            get { return m_propertyInfo; }
        }
        public string XPath {
            get { return m_xpath; }
        }
    }
}
